]> git.r.bdr.sh - rbdr/map/blame - Map/Presentation/Base Components/MapTextEditor.swift
Add search
[rbdr/map] / Map / Presentation / Base Components / MapTextEditor.swift
CommitLineData
98f09799
RBR
1/*
2 Copyright (C) 2024 Rubén Beltrán del Río
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see https://map.tranquil.systems.
16 */
5e8ff485
RBR
17import Cocoa
18import SwiftUI
19
20class MapTextEditorController: NSViewController {
21
e2c37ac1 22 @Binding var document: MapDocument
14491563
RBR
23 var highlightRanges: [Range<String.Index>] {
24 didSet {
25 updateHighlights()
26 }
27 }
28
29 var selectedRange: Int {
30 didSet {
31 updateHighlights()
32 focusOnResult()
33 }
34 }
35
fdb4633d 36 let onChange: () -> Void
5e8ff485 37
77d0155b
RBR
38 private let vertexRegex = MapParsingPatterns.vertex
39 private let edgeRegex = MapParsingPatterns.edge
40 private let blockerRegex = MapParsingPatterns.blocker
41 private let opportunityRegex = MapParsingPatterns.opportunity
fdb4633d 42 private let noteRegex = MapParsingPatterns.note
77d0155b 43 private let stageRegex = MapParsingPatterns.stage
e2c37ac1 44 private let groupRegex = MapParsingPatterns.group
77d0155b 45
fdb4633d 46 private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
77d0155b 47
14491563
RBR
48 init(
49 document: Binding<MapDocument>, highlightRanges: [Range<String.Index>], selectedRange: Int,
50 onChange: @escaping () -> Void
51 ) {
e2c37ac1 52 self._document = document
fdb4633d 53 self.onChange = onChange
14491563
RBR
54 self.highlightRanges = highlightRanges
55 self.selectedRange = selectedRange
5e8ff485
RBR
56 super.init(nibName: nil, bundle: nil)
57 }
58
59 required init?(coder: NSCoder) {
60 fatalError("init(coder:) has not been implemented")
61 }
62
63 override func loadView() {
64 let scrollView = NSTextView.scrollableTextView()
65 let textView = scrollView.documentView as! NSTextView
66
67 scrollView.translatesAutoresizingMaskIntoConstraints = false
68
e2c37ac1 69 textView.backgroundColor = .ui.background
75a0e450 70 textView.allowsUndo = true
5e8ff485 71 textView.delegate = self
77d0155b 72 textView.textStorage?.delegate = self
e2c37ac1 73 textView.string = self.document.text
5e8ff485
RBR
74 textView.isEditable = true
75 textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
76 self.view = scrollView
77 }
78
79 override func viewDidAppear() {
80 self.view.window?.makeFirstResponder(self.view)
14491563
RBR
81 updateHighlights()
82 }
83
84 private var textView: NSTextView? {
85 return (view as? NSScrollView)?.documentView as? NSTextView
86 }
87
88 private func updateHighlights() {
89 if let textView {
90 if let textStorage = textView.textStorage {
91 textStorage.removeAttribute(
92 .backgroundColor, range: NSRange(location: 0, length: textStorage.length))
93
94 for range in highlightRanges {
95 let nsRange = NSRange(range, in: textStorage.string)
96
97 textStorage.addAttribute(.backgroundColor, value: NSColor.syntax.match, range: nsRange)
98 }
99
100 textView.needsDisplay = true
101
102 }
103 }
104 }
105
106 private func focusOnResult() {
107 if let textView {
108 if let textStorage = textView.textStorage {
109 if selectedRange < highlightRanges.count {
110 let range = highlightRanges[selectedRange]
111 let nsRange = NSRange(range, in: textStorage.string)
112 textView.scrollRangeToVisible(nsRange)
113 textView.selectedRange = nsRange
114 }
115 }
116 }
117 }
118
119 private func setSelectionColor() {
120 guard let textView = self.textView else { return }
121
122 var selectedTextAttributes = textView.selectedTextAttributes
123 selectedTextAttributes[.backgroundColor] = NSColor.yellow.withAlphaComponent(0.3)
124 textView.selectedTextAttributes = selectedTextAttributes
5e8ff485
RBR
125 }
126}
127
128extension MapTextEditorController: NSTextViewDelegate {
129
130 func textDidChange(_ obj: Notification) {
131 if let textField = obj.object as? NSTextView {
e2c37ac1
RBR
132 self.document.text = textField.string
133
134 changeDebouncer.debounce {
135 DispatchQueue.main.async {
136 self.onChange()
fdb4633d 137 }
e2c37ac1 138 }
5e8ff485
RBR
139 }
140 }
141
142 func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool
143 {
144 let range = Range(shouldChangeTextIn, in: view.string)
145 let target = view.string[range!]
146
147 if target == "--" {
148 return false
149 }
150
151 return true
152 }
153}
154
77d0155b 155extension MapTextEditorController: NSTextStorageDelegate {
fdb4633d 156
77d0155b
RBR
157 override func textStorageDidProcessEditing(_ obj: Notification) {
158 if let textStorage = obj.object as? NSTextStorage {
fdb4633d 159 self.colorizeText(textStorage: textStorage)
77d0155b
RBR
160 }
161 }
162
163 private func colorizeText(textStorage: NSTextStorage) {
164 let range = NSMakeRange(0, textStorage.length)
165 var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
77d0155b
RBR
166
167 for match in matches {
e2c37ac1
RBR
168 textStorage.addAttributes(
169 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
170 textStorage.addAttributes(
171 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
172 textStorage.addAttributes(
173 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
174 textStorage.addAttributes(
175 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 4))
77d0155b
RBR
176 }
177
178 matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
179
180 for match in matches {
e2c37ac1
RBR
181 textStorage.addAttributes(
182 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
77d0155b
RBR
183 let arrowRange = match.range(at: 2)
184 textStorage.addAttributes(
fdb4633d 185 [.foregroundColor: NSColor.syntax.symbol],
77d0155b 186 range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
e2c37ac1
RBR
187 textStorage.addAttributes(
188 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 3))
77d0155b
RBR
189 }
190
191 matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
192
193 for match in matches {
e2c37ac1
RBR
194 textStorage.addAttributes(
195 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
196 textStorage.addAttributes(
197 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
198 textStorage.addAttributes(
199 [.foregroundColor: NSColor.syntax.symbol], range: match.range(at: 3))
200 textStorage.addAttributes(
201 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 4))
77d0155b
RBR
202 }
203
204 matches = blockerRegex.matches(in: textStorage.string, options: [], range: range)
205
206 for match in matches {
e2c37ac1
RBR
207 textStorage.addAttributes(
208 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
209 textStorage.addAttributes(
210 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
fdb4633d 211 }
e2c37ac1 212
fdb4633d
RBR
213 matches = noteRegex.matches(in: textStorage.string, options: [], range: range)
214
215 for match in matches {
e2c37ac1
RBR
216 textStorage.addAttributes(
217 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
218 textStorage.addAttributes(
219 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
220 textStorage.addAttributes(
221 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
77d0155b
RBR
222 }
223
224 matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
225
226 for match in matches {
e2c37ac1
RBR
227 textStorage.addAttributes(
228 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
229 textStorage.addAttributes(
230 [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
231 }
232
233 matches = groupRegex.matches(in: textStorage.string, options: [], range: range)
234
235 for match in matches {
236 textStorage.addAttributes(
237 [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
238 textStorage.addAttributes(
239 [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
77d0155b
RBR
240 }
241 }
242}
243
5e8ff485
RBR
244struct MapTextEditor: NSViewControllerRepresentable {
245
e2c37ac1 246 @Binding var document: MapDocument
14491563
RBR
247 var highlightRanges: [Range<String.Index>]
248 var selectedRange: Int
fdb4633d 249 var onChange: () -> Void = {}
5e8ff485
RBR
250
251 func makeNSViewController(
252 context: NSViewControllerRepresentableContext<MapTextEditor>
253 ) -> MapTextEditorController {
14491563
RBR
254 return MapTextEditorController(
255 document: $document, highlightRanges: highlightRanges, selectedRange: selectedRange,
256 onChange: onChange)
5e8ff485
RBR
257 }
258
259 func updateNSViewController(
260 _ nsViewController: MapTextEditorController,
261 context: NSViewControllerRepresentableContext<MapTextEditor>
14491563
RBR
262 ) {
263 nsViewController.highlightRanges = highlightRanges
264 nsViewController.selectedRange = selectedRange
265 }
5e8ff485 266}